package erfgame.core.world.terrain.file;

import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;


import erfgame.core.Point3DAndRotation;
import erfgame.core.Rotation;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.graph.simple.Node;
import erfgame.core.world.terrain.BlockSurfaceImages;
import erfgame.core.world.terrain.PixelDepth;
import erfgame.core.world.terrain.SurfaceImage;
import erfgame.core.world.terrain.insertion.SurfaceInsertion;
import erfgame.core.world.terrain.insertion.SurfaceTerrainImageInsertion;

// TODO : this has double compression, need to evaluate if this is good for performance
public class FileBlockSurfaceImagesDAO implements ObjectDAO<Point3DAndRotation, BlockSurfaceImages> {
	
	private static final String EXTENSION = ".bsi";
	private static final int MAX_COMPRESSION = 256;
	
	private static final int FLAG_EMPTY = 0x01;
	
	private static final Logger log = Logger.getLogger( FileBlockSurfaceImagesDAO.class.getName() );
	
	private static final int TYPE_SURFACE_IMAGE = 0;

	private File baseDirectory;
	private int surfaceWidth;
	private int surfaceHeight;
	
	public FileBlockSurfaceImagesDAO( File baseDirectory, int surfaceWidth, int surfaceHeight ) {
		this.baseDirectory = baseDirectory;
		this.surfaceWidth = surfaceWidth;
		this.surfaceHeight = surfaceHeight;
	}
	
	public boolean containsObject(Point3DAndRotation key) {
		return getFile( key.x, key.y, key.z, key.rotation ).exists();
	}

	public boolean removeObject(Point3DAndRotation key) {
		return false;
	}

	public BlockSurfaceImages retrieveObject(Point3DAndRotation key) {
		return findBlockSurfaceImages( key, null, null );
	}

	public void storeObject(Point3DAndRotation key, BlockSurfaceImages object) {
		storeBlockSurfaceImages( key.x, key.y, key.z, key.rotation, object );
	}

	public final BlockSurfaceImages findBlockSurfaceImages(Point3DAndRotation key, PixelDepth[][] intoDepths, int[][] intoSurfaces ) {
		
		BlockSurfaceImages result;
		File file = getFile( key.x, key.y, key.z, key.rotation );
		InputStream fis = null;
		try {
			fis = new GZIPInputStream( new FileInputStream( file ) );
			int flags = fis.read();
			if( (flags & FLAG_EMPTY) == 0 ) {
				if( intoDepths == null ) {
					intoDepths = new PixelDepth[ surfaceWidth ][ surfaceHeight ];
				}
				if( intoSurfaces == null ) {
					intoSurfaces = new int[ surfaceWidth ][ surfaceHeight ];
				}
				BufferedInputStream bis = new BufferedInputStream( fis );
				DataInputStream dis = new DataInputStream( bis );
				for( int sx=this.surfaceWidth; sx>0; ) {
					sx--;
					PixelDepth[] intoDepthsX = intoDepths[sx];
					for( int sy=this.surfaceHeight; sy>0; ) {
						sy--;
						int firstGridId = dis.read();
						PixelDepth depth;
						if( firstGridId == ((int)Byte.MAX_VALUE) ) {
							depth = null;
						} else {
							int firstDepthStart = dis.read();
							int firstDepthEnd = dis.read();
							int numDepths = dis.read();
							depth = new PixelDepth(
									firstDepthStart, 
									firstDepthEnd, 
									(byte)firstGridId, 
									numDepths
							);
						}
						intoDepthsX[sy] = depth;
					}
				}
				
				int maxSurfaceId = 0;
				for( int sx=this.surfaceWidth; sx>0; ) {
					sx--;
					int[] intoSurfacesX = intoSurfaces[sx];
					for( int sy=this.surfaceHeight; sy>0; ) {
						sy--;
						// TODO : the use of byte means we can only have 255 images (hopefully will be enough) and need unique surface ids for all
						// will need to reload the images when we hit that number
						intoSurfacesX[sy] = dis.read();
					}
				}
				int numSurfaceImages = dis.readShort();
				ArrayList<Node<SurfaceInsertion>> insertions = new ArrayList<Node<SurfaceInsertion>>( numSurfaceImages );
				int[] rgbArray = new int[ MAX_COMPRESSION ];
				result = new BlockSurfaceImages( 
						maxSurfaceId+1, 
						insertions, 
						intoDepths, 
						intoSurfaces 
				);
				for( int i=numSurfaceImages; i>0; ) {
					i--;
					SurfaceInsertion insertion;
					int type = dis.read();
					switch( type ) {
					case TYPE_SURFACE_IMAGE:
						int px = dis.read();
						int py = dis.read();
						int surfaceId = dis.readShort();
						int blockGridId = dis.read();
						// the image reader doesn't know when to stop reading.
						// TODO : perhaps we should reuse this image data array?
						BufferedImage image = readImage( dis, rgbArray );
						SurfaceImage surfaceImage = new SurfaceImage( px, py, surfaceId, image, (byte)blockGridId );
						insertion = new SurfaceTerrainImageInsertion( result, surfaceImage );
						Node<SurfaceInsertion> node = new Node<SurfaceInsertion>( insertion );
						insertions.add( node );
						break;
					default:
						throw new Exception( "unknown type "+type );
					}
					
				}
			} else {
				result = new BlockSurfaceImages();
			}
		} catch( Exception ex ) {
			throw new RuntimeException( "unable to read from "+file.getPath(), ex );
		} finally {
			if( fis != null ) {
				try {
					fis.close();
				} catch( IOException ex ) {
					ex.printStackTrace();
				}
			}
		}
		return result;
	}

	public final void storeBlockSurfaceImages(int x, int y, int z, Rotation rotation,
			BlockSurfaceImages bsi) {
		File file = getFile( x, y, z, rotation );
		OutputStream fos = null;
		try {
			fos = new GZIPOutputStream( new FileOutputStream( file ) );

			List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
			List<Node<SurfaceInsertion>> validInsertions = new ArrayList<Node<SurfaceInsertion>>( insertions.size() );
			for( int i=0; i<insertions.size(); i++ ) {
				Node<SurfaceInsertion> node = insertions.get( i );
				SurfaceInsertion insertion = node.getValue();
				if( insertion instanceof SurfaceTerrainImageInsertion ) {
					validInsertions.add( node );
				}
			}
			if( validInsertions.size() > BlockSurfaceImages.MAX_SURFACES ) {
				log.warning("writing back "+validInsertions.size()+" images to block ("+x+","+y+","+z+"), this is a large number and may cause performance problems" );
			}
			if( validInsertions.size() > 0 ) {
				BufferedOutputStream bos = new BufferedOutputStream( fos );
				DataOutputStream dos = new DataOutputStream( bos );
				dos.write( 0 );
				PixelDepth[][] pixelDepths = bsi.getPixelDepths();
				for( int sx=this.surfaceWidth; sx>0; ) {
					sx--;
					PixelDepth[] pixelDepthsX = pixelDepths[sx];
					for( int sy=this.surfaceHeight; sy>0; ) {
						sy--;
						PixelDepth depth = pixelDepthsX[sy];
						if( depth != null ) {
							dos.write( depth.getFirstBlockGridId() );
							dos.write( depth.getFirstDepthStart() );
							dos.write( depth.getFirstDepthEnd() );
							dos.write( depth.getNumDepths() );
						} else {
							dos.write( Byte.MAX_VALUE );
						}
					}
				}
				
				
				int[][] surfaces = bsi.getSurfaces();
				for( int sx=this.surfaceWidth; sx>0; ) {
					sx--;
					int[] surfacesX = surfaces[sx];
					for( int sy=this.surfaceHeight; sy>0; ) {
						sy--;
						dos.write( surfacesX[sy] );
					}
				}
				
				dos.writeShort( validInsertions.size() );
				int[] rgbArray = new int[ this.surfaceHeight * this.surfaceWidth ];
				for( int i=0; i<validInsertions.size(); i++ ) {
					Node<SurfaceInsertion> node = validInsertions.get( i );
					SurfaceInsertion insertion = node.getValue();
					if( insertion instanceof SurfaceTerrainImageInsertion ) {
						dos.write( TYPE_SURFACE_IMAGE );
						SurfaceTerrainImageInsertion terrainInsertion = ((SurfaceTerrainImageInsertion)insertion);
						SurfaceImage surfaceImage = terrainInsertion.getSurfaceImage();
						dos.write( surfaceImage.getPx() );
						dos.write( surfaceImage.getPy() );
						
						int surfaceId = surfaceImage.getSurfaceId();
						dos.writeShort( surfaceId );
						dos.write( surfaceImage.getBlockGridId() );
						writeImage( dos, surfaceImage.getImage(), rgbArray );					
					} else {
						throw new RuntimeException( "unrecognised insertion "+insertion );
					}
				}
				dos.flush();
			} else {
				fos.write( FLAG_EMPTY );
			}
		} catch( IOException ex ) {
			throw new RuntimeException( "unable to write to "+file.getPath(), ex );
		} finally {
			if( fos != null ) {
				try {
					fos.close();
				} catch( IOException ex ) {
					ex.printStackTrace();
				}
			}
		}
	}
	
	private final void writeImage( DataOutputStream dos, BufferedImage image, int[] rgbArray ) 
		throws IOException {
		// store image uncompressed for speed?
		dos.write( image.getWidth() );
		dos.write( image.getHeight() );
		compressImage( dos, image, rgbArray );
		
	}
	
	private final void compressImage( DataOutputStream dos, BufferedImage image, int[] rgbArray ) 
		throws IOException {
		// just use lwz for now
		int width = image.getWidth();
		int height = image.getHeight();
		int scansize = width;
		image.getRGB(0, 0, width, height, rgbArray, 0, scansize );
		int index = width * height - 1;
		int previousColor = rgbArray[ index ];
		int count = 1;
		while( index > 0 ) {
			index--;
			int color = rgbArray[ index ];
			if( color != previousColor || count >= MAX_COMPRESSION ) {
				dos.write( count - 1 );
				dos.writeInt( previousColor );
				count = 1;
				previousColor = color;
			} else {
				count ++;
			}
		}
		if( count > 0 ) {
			dos.write( count - 1 );
			dos.writeInt( previousColor );
		}
	}
	
	private final BufferedImage readImage( DataInputStream dis, int[] rgbArray ) 
		throws IOException {
		int width = dis.read();
		int height = dis.read();
		BufferedImage image = new BufferedImage( 
				width, 
				height, 
				BufferedImage.TYPE_INT_ARGB
		);
		uncompressImage( dis, image, width, height, rgbArray );
		return image;
	}
	
	private final void uncompressImage( DataInputStream dis, BufferedImage image, int width, int height, int[] rgbArray ) 
		throws IOException {
		int count = width * height;
		while( count > 0 ) {
			int quantity = dis.read() + 1;
			int argb = dis.readInt();
			// write from 0 to final x
			int finishX = (count-1) % width;
			int finishY = (count-1) / width;
			count -= quantity;
			int startX = count % width;
			int startY = count / width;
			if( startY == finishY ) {
				Arrays.fill( rgbArray, 0, quantity, argb );
				image.setRGB(
						startX, 
						startY, 
						quantity, 
						1, 
						rgbArray, 
						0, 
						quantity
				);
			} else {
				Arrays.fill( rgbArray, 0, width, argb );
				int startWidth = width - startX;
				image.setRGB(
						startX, 
						startY, 
						startWidth, 
						1, 
						rgbArray, 
						0, 
						startWidth
				);
				int finishWidth = finishX + 1;
				image.setRGB(
						0, 
						finishY, 
						finishWidth, 
						1, 
						rgbArray, 
						0, 
						finishWidth
				);
				int middleHeight = finishY - startY - 1;
				if( middleHeight > 0 ) {
					int middleY = startY + middleHeight;
					for( int i=middleHeight; i>0;  ) {
						i--;
						image.setRGB(
								0, 
								middleY,
								width, 
								1,
								rgbArray, 
								0, 
								width
						);
						middleY--;
					}
				}
			}
		}
	}

	private final File getFile( int x, int y, int z, Rotation rotation ) {
		StringBuffer fileName = new StringBuffer(30);
		fileName.append( x );
		fileName.append( 'x' );
		fileName.append( y );
		fileName.append( 'x' );
		fileName.append( z );
		fileName.append( '_' );
		fileName.append( rotation.getDegrees() );
		fileName.append( EXTENSION );
		File file = new File( this.baseDirectory, fileName.toString() );
		return file;
	}
}
